【Greengrass V2】定期的にセンサーデータを取得する Lambda コンポーネントを AWS SAM で作ってみた
前回の記事では、カスタムコンポーネントで温湿度データを取得しましたが、今回はこれを Lambda として作成してみました。
全体の構成は下記のようになります。(デバイス間の結線は簡略化して描いています。)
Lambda 関数を作るのはどんな方法でも構いませんが、今回は AWS SAM を使うことにしました。Lambda による処理の内容は下記のとおりです。
- 前回の記事にあるコードをベースに Modbus-RTU 経由で温湿度データを取得
- 取得した温湿度データを所定のファイルに出力
/tmp/Greengrass_modbus_sensor_lambda.log
- 10秒間隔でデータを取得してファイル出力
なお、デバイス側の設定は前回の記事の内容を踏襲するものとします。
(ggc_user
の所属グループの変更や pymodbus
のインストール等です)
Raspberry Pi の Python バージョン
こちらも前回と変更はありません。
$ python -V Python 2.7.16 $ python3 -V Python 3.7.3
上記のとおりデフォルトのバージョンは 2.7.16
ですが、Lambda のランタイムバージョンに合わせて python3 をデフォルトにする必要は有りませんでした。(動作確認の節も合わせてご確認ください)
Lambda 関数の作成
まずは Lambda 関数を作っていきます。AWS SAM に関する説明は割愛させていただきます。
作業は AWS SAM をインストールした Mac で行います。
sam init
Raspberry Pi 側の Python は 3.7.3
なのでランタイムも同じバージョンを指定しています。
$ sam init \ --runtime python3.7 \ --name modbus-sensor-lambda-component \ --app-template hello-world \ --package-type Zip
SAM テンプレート
Lambda コンポーネントではバージョン指定が必要なので、AutoPublishAlias
を指定しています。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > modbus-sensor-lambda-component Sample SAM Template for modbus-sensor-lambda-component Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.7 AutoPublishAlias: dev Outputs: HelloWorldFunction: Description: "Hello World Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" Value: !GetAtt HelloWorldFunctionRole.Arn
Lambdaコード
from pymodbus.client.sync import ModbusSerialClient as ModbusClient import datetime import time def run_sync_client(): client = ModbusClient(baudrate=9600, port="/dev/ttyUSB0", method="rtu") client.connect() rr = client.read_input_registers(address=1, count=2, unit=0x1) temperature = rr.registers[0]/10 humidity = rr.registers[1]/10 # Append the message to the log file. with open('/tmp/Greengrass_modbus_sensor_lambda.log', 'a') as f: print(f"{str(datetime.datetime.now())}\tTemperatur:\t{temperature} ℃", file=f) print(f"{str(datetime.datetime.now())}\tHumidity:\t{humidity} %", file=f) client.close() while True: run_sync_client() time.sleep(10) def lambda_handler(event, context): return
この Lambda コードは Greengrass の設定で 「存続期間の長い Lambda (Long-Lived Lambda)」になっていることを想定したものになっています。
Long-Lived Lambda の場合、トピックに対するメッセージを受信しなくても、デプロイされた後に自動的に Lambda 関数が実行されます。ハンドラーの前にある初期化処理でループ処理を実装することで定期処理を行っています。
「存続期間の長い Lambda」について
Greengrass V2 の Lambda コンポーネントでは、デフォルトで 「存続期間の長い Lambda」 としてデプロイされます。「存続期間が長い」という表現は他にも「Long-Lived」や「Pinned」と表現されることがあります。
個人的には「Long-Lived Lambda」と表現するのが分かりやすいかなと思っています。
Long-Lived Lambda の詳細については下記の Blackbelt の資料が分かりやすいです。
確認した限りでは、Greengrass V1 と V2 で Long-Lived Lambda の仕様の違いは無いようです。合わせて下記も参考になるかと思います。
- LambdaExecutionParameters - AWS IoT Greengrass V2
pinned
の項目を参照
- Run Lambda functions on the AWS IoT Greengrass core - AWS IoT Greengrass V1
Lifecycle configuration for Greengrass Lambda functions
の項目を参照
ビルド
利用しているラズパイ上の Python のバージョンは 3.7.3
なので、同じバージョンのビルドイメージを使ってビルドしました。
(Docker Desktop for Mac がインストール済みで起動している前提です)
$ sam build \ --use-container \ --build-image Function1=amazon/aws-sam-cli-build-image-python3.7
デプロイ
ビルドできたらデプロイします。アーティファクトが置かれる S3 バケットはご利用の任意のものを指定いただければと思います。
$ sam package \ --output-template-file packaged.yaml \ --s3-bucket xxxxxxxxxx
$ sam deploy \ --template-file packaged.yaml \ --stack-name modbus-sensor-lambda-component-stack \ --s3-bucket xxxxxxxxxx \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset
Lambda コンポーネントの作成
Lambda関数を作成できたら、Greengrass サービスからデバイスに Lambda 関数をデプロイします。
最初にコンポーネントを作成します。
次の画面で 「Lambda 関数 をインポートする」を選択します。
その下のプルダウンから先程 AWS SAM で作成した Lambda 関数を選択してください。AWS SAM でバージョンも付与されているので最新バージョンを選択します。(環境によりデプロイしたいバージョンを指定してください)
ちなみに、対象の Lambda のコンポーネント作成が初回の場合、コンポーネントバージョンを指定しなければ Lambda のバージョンがコンポーネントバージョンになります。
(例:Lambda バージョンが 5
なら、コンポーネントバージョンは 5.0.0
)
オプション設定は全てデフォルトのままとしました。下記の「固定済み」の指定では Lambda の実行方式を選択します。デフォルトで「Long-Lived」 が選択されているので変更せずにそのままとします。
次のオプション設定では、Lambda コンポーネントのコンテナ化の設定になります。デフォルトでは Greengrass コンテナ内で実行されますが「Greengrass コンテナ」を選択すると、アクセスを許可したいデバイスのパスやボリュームを指定してセキュアに Lambda コンポーネントを稼働させることができます。
今回は動作確認を優先したかったので「コンテナなし」 を選択しました。この場合は Lambda コンポーネントから任意のデバイスやボリュームにアクセスできます。
最後に「コンポーネントを作成」 をクリックします。
Lambda コンポーネントのデプロイ
コンポーネントが作成できたら、作成したコンポーネントの画面に遷移するのでそのまま「デプロイ」をクリックしてデプロイ作業に進みましょう。
次の画面ではデプロイしたい対象を選択します。下記の記事のようにデバイスに Greengrass Core ソフトウェアをデプロイしていれば、そのデプロイが存在します。
次の画面ではそのまま次へ進みます。(名前を変えたりタグを付けたい場合は設定してください)
「コンポーネントの選択」画面では、デプロイしたいコンポーネントを選択します。デプロイしたい Lambda コンポーネントがデフォルトで選択されているので、そのまま次へ進みます。
次の画面は今回は特にやることが無いので、そのまま次に進みます。
Greengrass V2 のコンポーネントデプロイでは、AWS IoT のジョブ機能が使われています。ここではそのジョブの設定を行います。今回はデフォルトで進めました。
最後にレビュー画面で設定を確認します。
レビューで問題なければ画面下にある「デプロイ」をクリックしてデプロイを開始します。
先程書いたように「ジョブ機能」を使ってデプロイされるので、ジョブの画面でデプロイ状況の詳細を見ることができます。
デプロイが成功しました。
動作確認
無事にデプロイできたか Raspberry Pi 上で確認します。
$ sudo /greengrass/v2/bin/greengrass-cli component list
コンポーネントの一覧から Lambda コンポーネントを確認できました。
(結果を一部抜粋) Component Name: modbus-sensor-lambda-component--HelloWorldFunction-xxxxxxxxxxxx Version: 1.0.0 State: RUNNING Configuration: {"containerMode":"NoContainer","containerParams":{"devices":{},"memorySize":16000.0,"mountROSysfs":false,"volumes":{}},"inputPayloadEncodingType":"json","lambdaExecutionParameters":{"EnvironmentVariables":{}},"maxIdleTimeInSeconds":60.0,"maxInstancesCount":100.0,"maxQueueSize":1000.0,"pinned":true,"pubsubTopics":{},"statusTimeoutInSeconds":60.0,"timeoutInSeconds":3.0}
Lambda のコード内で指定したファイルを確認すると、きちんと温湿度が記録されていました!
$ tail -f /tmp/Greengrass_modbus_sensor_lambda.log 2021-08-18 20:30:47.752837 Temperatur: 27.9 ℃ 2021-08-18 20:30:47.752924 Humidity: 56.4 % 2021-08-18 20:30:57.918288 Temperatur: 27.9 ℃ 2021-08-18 20:30:57.918383 Humidity: 56.4 % 2021-08-18 20:31:07.966270 Temperatur: 27.9 ℃ 2021-08-18 20:31:07.966368 Humidity: 56.4 % 2021-08-18 20:31:18.137136 Temperatur: 27.9 ℃ 2021-08-18 20:31:18.137234 Humidity: 56.4 % 2021-08-18 20:31:28.185414 Temperatur: 27.9 ℃ 2021-08-18 20:31:28.185510 Humidity: 56.4 %
プロセスを確認すると、Raspberry Pi 上のデフォルトの Python は 2.7.16
ですが
下記のように /usr/bin/python3.7
が使われていました。
$ ps aux | grep modbus-sensor-lambda-component ggc_user 10326 0.1 0.3 29952 14756 ? Sl 16:14 0:00 /usr/bin/python3.7 -u /greengrass/v2/work/modbus-sensor-lambda-component--HelloWorldFunction-xxxxxxxxxxxx/work/worker/0/runtime/python/lambda_runtime.py --handler=app.lambda_handler
試しに Lambda のランタイムを python3.8
にして試したところ、コンポーネントのデプロイは正常終了しますが、Raspberry Pi に Python3.8 がインストールされていないので実際の処理は正常に動いていないようでした。
最後に
従来の Lambda による開発手法がそのまま使える点はメリットだと思いますが、V1 の時と同様に下記のような点に注意が必要となります。
- 動作確認のために毎回デバイスへのデプロイが必要
- Lambda の ライフサイクルを意識した実装が必要
- Lambda のコンテナ設定のためにデバイス側の構成を把握しておく必要がある
一方で、上記の点は逆にメリットとなる側面もあるため、カスタムコンポーネントと Lambda コンポーネントのどちらがいいのか、という点は総合的な観点で判断する必要があると思いました。
ノウハウ溜まってきたら改めてまとめてみたいと思います。
以上です。